﻿using System;
using System.Collections.Generic;
using GeoRef.GeoQueries;
using GeoRef.Properties;

namespace GeoRef
{
    /// <summary>
    /// The georef non-graphical interface.
    /// </summary>
    public class GeoQuery
    {
        #region fields

        /// <value>
        /// The appid value for Yahoo webservices.
        /// </value>
        private string yahooAppid;

        /// <value>
        /// The geocoding webservice url.
        /// </value>
        private string geoCodingWebServiceUrl;

        /// <value>
        /// The reverse geocoding webservice url.
        /// </value>
        private string reverseGeoCodUrl;

        #endregion

        #region GeoQuery Constructor

        /// <summary>
        /// Default constructor for the GeoQuery class.
        /// </summary>
        public GeoQuery()
        {
            yahooAppid = Settings.Default.YahooAppid;
            geoCodingWebServiceUrl = Settings.Default.GeoCodingWebServiceUrl;
            reverseGeoCodUrl = Settings.Default.ReverseGeocoding;
        }

        #endregion

        #region geocoding methods

        /// <summary>
        /// This method performs a geocoding operation given four strings.
        /// </summary>
        /// <param name="state">The state name (or null)</param>
        /// <param name="city">The city's name (or null)</param>
        /// <param name="zipCode">The zip code (or null)</param>
        /// <param name="street">The street's name (or null)</param>
        /// <param name="location">A complete address. If this is present,
        /// all other parameters will be ignored</param>
        /// <exception cref="GeoRef.Exceptions.GeoCodingException">If something goes wrong with the geocoding.</exception>
        /// <returns>A List&lt;<see cref="GeoLoc" />&gt; object that encapsulates the geocoding result.</returns>
        public List<GeoLoc> geoCoding(string state, string city, string zipCode, string street, string location)
        {
            GeoCodingQuery gcq = new GeoCodingQuery();
            gcq.AppID = yahooAppid;
            gcq.GeoCodingWebServiceUrl = geoCodingWebServiceUrl;
            gcq.State = state;
            gcq.City = city;
            gcq.Street = street;
            gcq.ZipCode = zipCode;
            gcq.Location = location;
            return gcq.getGeocoding();
        }

        /// <summary>
        /// This method performs a geocoding operation given four strings.
        /// </summary>
        /// <param name="state">The state name (or null)</param>
        /// <param name="city">The city's name (or null)</param>
        /// <param name="zipCode">The zip code (or null)</param>
        /// <param name="street">The street's name (or null)</param>
        /// <exception cref="GeoRef.Exceptions.GeoCodingException">If something goes wrong with the geocoding.</exception>
        /// <returns>A List&lt;<see cref="GeoLoc" />&gt; object that encapsulates the geocoding result.</returns>
        public List<GeoLoc> geoCoding(string state, string city, string zipCode, string street)
        {
            return geoCoding(state, city, zipCode, street, null);
        }

        /// <summary>
        /// Performs a geocoding operation given a string.
        /// </summary>
        /// <param name="location">A complete address.</param>
        /// <exception cref="GeoRef.Exceptions.GeoCodingException">If something goes wrong with the geocoding.</exception>
        /// <returns>A List&lt;<see cref="GeoLoc" />&gt; object that encapsulates the geocoding result.</returns>
        /// <example>
        /// <code>
        /// GeoQuery q = new GeoQuery();
        /// GeoLoc loc = q.geoCoding("via dini 2, Pisa, Italy");
        /// GeoLoc l2 = q.geoCoding("via martiri 1 Pisa Italia");
        /// </code>
        /// </example>
        public List<GeoLoc> geoCoding(string location)
        {
            return (geoCoding(null, null, null, null, location));
        }

        /// <summary>
        /// Performs a geocoding operation given two strings: a state name and a city name.
        /// </summary>
        /// <param name="state">The state name.</param>
        /// <param name="city">The city name.</param>
        /// <exception cref="GeoRef.Exceptions.GeoCodingException">If something goes wrong with the geocoding.</exception>
        /// <returns>A List&lt;<see cref="GeoLoc" />&gt; object that encapsulates the geocoding result.</returns>
        public List<GeoLoc> geoCoding(string state, string city)
        {
            return (geoCoding(state, city, null, null, null));
        }

        /// <summary>
        /// Performs a geocoding operation given three strings: a state, a city and a street.
        /// </summary>
        /// <param name="state">The state name.</param>
        /// <param name="city">The city name.</param>
        /// <param name="street">The street name.</param>
        /// <exception cref="GeoRef.Exceptions.GeoCodingException">If something goes wrong with the geocoding.</exception>
        /// <returns>A List&lt;<see cref="GeoLoc" />&gt; object that encapsulates the geocoding result.</returns>
        public List<GeoLoc> geoCoding(string state, string city, string street)
        {
            return (geoCoding(state, city, null, street, null));
        }

        /// <summary>
        /// Performs a reverse geocoding operation given a location
        /// </summary>
        /// <param name="latitude">Latitude of request location</param>
        /// <param name="longitude">Longitude of request location</param>
        /// <returns>A <see cref="GeoLoc" /> that represents the query result</returns>
        public GeoLoc reverseGeoCoding(double latitude, double longitude) {
            double newLat, newLong;
            string addr;
            GeoLoc result;            
            int indexNearest = 0;
            double nearestLoc = 0.0;
            List<GeoLoc> matchingAddresses = new List<GeoLoc>();
            //performs a query to retrieve address for the given coordinates
            ReverseGeoCodingQuery query = new ReverseGeoCodingQuery(reverseGeoCodUrl);
            addr = query.ReverseGeoCode(latitude, longitude, out newLat, out newLong);
            //if the result is not exactly equal to the searched point, then adds a warning to result description
            if (latitude != newLat || longitude != newLong) {
                result = new GeoLoc(newLat, newLong, addr, "Warning: the exact location could not be found; here is the nearest address.");                
            } else {
                result = new GeoLoc(latitude, longitude, addr, "");                
            }
            try {
                /*since two different services are used for direct and reverse geocoding
                 * and they have different address format, we unify the format performing
                 * a direct geocoding e taking as final result the nearest one*/
                matchingAddresses = geoCoding(addr);                
                indexNearest = 0;
                //distance calculated using manhattan distance
                nearestLoc = (Math.Abs(matchingAddresses[0].Lat - latitude) + Math.Abs(matchingAddresses[0].Lng - longitude));
                for (int i = 1; i < matchingAddresses.Count; i++ ) {
                    if ((Math.Abs(matchingAddresses[i].Lat - latitude) + Math.Abs(matchingAddresses[i].Lng - longitude)) < nearestLoc) {
                        nearestLoc = (Math.Abs(matchingAddresses[i].Lat - latitude) + Math.Abs(matchingAddresses[i].Lng - longitude));
                        indexNearest = i;
                    }
                }                
                return matchingAddresses.ToArray()[indexNearest];                
            } catch {
                return result;
            }
            
        }
        
        #endregion
    }
}
